<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>一对一WebRTC视频通话</title>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background: linear-gradient(135deg, #1a2a6c, #b21f1f, #1a2a6c);
            color: white;
            min-height: 100vh;
            padding: 20px;
            display: flex;
            justify-content: center;
            align-items: center;
        }

        .container {
            width: 100%;
            max-width: 1200px;
            background: rgba(0, 0, 0, 0.5);
            border-radius: 20px;
            overflow: hidden;
            box-shadow: 0 15px 30px rgba(0, 0, 0, 0.4);
        }

        header {
            text-align: center;
            padding: 30px;
            background: rgba(0, 0, 0, 0.6);
        }

        h1 {
            font-size: 2.8rem;
            margin-bottom: 10px;
            text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
            background: linear-gradient(to right, #4facfe, #00f2fe);
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
        }

        .subtitle {
            font-size: 1.2rem;
            opacity: 0.9;
            color: #a0d2eb;
        }

        .main-content {
            display: flex;
            padding: 20px;
            gap: 20px;
        }

        .control-panel {
            flex: 1;
            min-width: 300px;
            background: rgba(0, 0, 0, 0.4);
            border-radius: 15px;
            padding: 25px;
            box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3);
        }

        .control-group {
            margin-bottom: 25px;
        }

        .control-title {
            display: flex;
            align-items: center;
            gap: 10px;
            margin-bottom: 15px;
            font-size: 1.2rem;
            font-weight: 600;
            color: #4facfe;
        }

        .control-title i {
            font-size: 1.5rem;
        }

        input, select {
            width: 100%;
            padding: 14px;
            border: none;
            border-radius: 10px;
            background: rgba(255, 255, 255, 0.9);
            font-size: 1.1rem;
            margin-bottom: 10px;
            transition: all 0.3s;
        }

        input:focus, select:focus {
            outline: none;
            box-shadow: 0 0 0 3px rgba(79, 172, 254, 0.5);
        }

        .button-group {
            display: flex;
            flex-wrap: wrap;
            gap: 15px;
            margin-top: 20px;
        }

        button {
            flex: 1;
            min-width: 120px;
            padding: 14px 20px;
            border: none;
            border-radius: 10px;
            cursor: pointer;
            font-size: 1.1rem;
            font-weight: 600;
            transition: all 0.3s;
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 8px;
            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
        }

        .btn-join {
            background: linear-gradient(to right, #4facfe, #00f2fe);
            color: white;
        }

        .btn-join:hover {
            transform: translateY(-3px);
            box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3);
        }

        .btn-leave {
            background: linear-gradient(to right, #ff416c, #ff4b2b);
            color: white;
        }

        .btn-leave:hover {
            transform: translateY(-3px);
            box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3);
        }

        button:disabled {
            background: #777;
            cursor: not-allowed;
            transform: none;
            box-shadow: none;
        }

        .video-container {
            flex: 2;
            display: flex;
            flex-direction: column;
            gap: 20px;
        }

        .video-row {
            display: flex;
            gap: 20px;
            height: 50%;
        }

        .video-card {
            flex: 1;
            min-width: 300px;
            background: rgba(0, 0, 0, 0.4);
            border-radius: 15px;
            overflow: hidden;
            box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3);
            transition: all 0.3s;
            display: flex;
            flex-direction: column;
        }

        .video-card:hover {
            transform: translateY(-5px);
            box-shadow: 0 12px 24px rgba(0, 0, 0, 0.4);
        }

        .video-header {
            padding: 15px 20px;
            background: rgba(0, 0, 0, 0.7);
            font-size: 1.3rem;
            font-weight: 600;
            text-align: center;
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 10px;
        }

        .local-video {
            border-top: 4px solid #4facfe;
        }

        .remote-video {
            border-top: 4px solid #00f2fe;
        }

        video {
            width: 100%;
            height: 100%;
            background: #000;
            display: block;
            flex-grow: 1;
        }

        .status-panel {
            background: rgba(0, 0, 0, 0.4);
            border-radius: 15px;
            padding: 20px;
            box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3);
        }

        .status-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 15px;
        }

        .status-header h3 {
            font-size: 1.4rem;
            color: #00f2fe;
            display: flex;
            align-items: center;
            gap: 10px;
        }

        .status-content {
            display: flex;
            gap: 20px;
        }

        .call-status {
            flex: 1;
            padding: 15px;
            background: rgba(0, 0, 0, 0.3);
            border-radius: 10px;
            min-height: 120px;
        }

        .status-indicator {
            display: flex;
            align-items: center;
            gap: 10px;
            margin-bottom: 15px;
            font-size: 1.2rem;
        }

        .indicator {
            width: 15px;
            height: 15px;
            border-radius: 50%;
            background: #777;
        }

        .indicator.connected {
            background: #00f2fe;
            box-shadow: 0 0 10px #00f2fe;
        }

        .indicator.calling {
            background: #ffcc00;
            box-shadow: 0 0 10px #ffcc00;
        }

        .logs {
            flex: 2;
            max-height: 120px;
            overflow-y: auto;
            background: rgba(0, 0, 0, 0.3);
            border-radius: 10px;
            padding: 15px;
        }

        .log-entry {
            margin: 8px 0;
            padding: 8px;
            border-radius: 6px;
            background: rgba(255, 255, 255, 0.1);
            font-size: 0.9rem;
            display: flex;
        }

        .log-time {
            color: #4facfe;
            margin-right: 12px;
            font-weight: 600;
            min-width: 70px;
        }

        .empty-logs {
            text-align: center;
            padding: 10px;
            color: #aaa;
        }

        @media (max-width: 900px) {
            .main-content {
                flex-direction: column;
            }

            .video-row {
                flex-direction: column;
            }

            h1 {
                font-size: 2.2rem;
            }
        }
    </style>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
</head>
<body>
<div id="app">
    <div class="container">
        <header>
            <h1><i class="fas fa-video"></i> 一对一WebRTC视频通话</h1>
            <p class="subtitle">基于Vue2和原生WebRTC API实现</p>
        </header>

        <div class="main-content">
            <div class="control-panel">
                <div class="control-group">
                    <div class="control-title">
                        <i class="fas fa-door-open"></i>
                        <span>房间设置</span>
                    </div>
                    <input type="text" v-model="roomId" placeholder="输入房间ID">
                    <input type="text" v-model="userId" placeholder="输入用户ID">
                </div>

                <div class="control-group">
                    <div class="control-title">
                        <i class="fas fa-camera"></i>
                        <span>设备选择</span>
                    </div>
                    <select v-model="selectedCamera">
                        <option v-for="device in cameraDevices" :value="device.deviceId">
                            {{ device.label || '摄像头 ' + ($index + 1) }}
                        </option>
                    </select>
                </div>

                <div class="button-group">
                    <button class="btn-join" @click="joinRoom" :disabled="isConnected">
                        <i class="fas fa-plug"></i>
                        <span>加入房间</span>
                    </button>
                    <button class="btn-leave" @click="leaveRoom" :disabled="!isConnected">
                        <i class="fas fa-sign-out-alt"></i>
                        <span>离开房间</span>
                    </button>
                </div>
            </div>

            <div class="video-container">
                <div class="video-row">
                    <div class="video-card local-video">
                        <div class="video-header">
                            <i class="fas fa-user"></i>
                            <span>本地视频 ({{ userId || '未加入' }})</span>
                        </div>
                        <video ref="localVideo" autoplay muted playsinline></video>
                    </div>

                    <div class="video-card remote-video" v-if="remoteUserId">
                        <div class="video-header">
                            <i class="fas fa-user-friends"></i>
                            <span>远程用户 ({{ remoteUserId }})</span>
                        </div>
                        <video ref="remoteVideo" autoplay playsinline></video>
                    </div>
                </div>

                <div class="status-panel">
                    <div class="status-header">
                        <h3><i class="fas fa-info-circle"></i> 系统状态</h3>
                    </div>

                    <div class="status-content">
                        <div class="call-status">
                            <div class="status-indicator">
                                <div class="indicator" :class="{
                    'connected': callStatus === 'connected',
                    'calling': callStatus === 'calling'
                  }"></div>
                                <div>{{ callStatusText }}</div>
                            </div>
                            <div v-if="remoteUserId">
                                <p><i class="fas fa-id-badge"></i> 远程用户: {{ remoteUserId }}</p>
                                <p><i class="fas fa-network-wired"></i> 连接类型: {{ connectionType }}</p>
                            </div>
                            <div v-else>
                                <p>等待对方加入房间...</p>
                            </div>
                        </div>

                        <div class="logs">
                            <div v-for="log in logs" :key="log.timestamp" class="log-entry">
                                <span class="log-time">{{ log.time }}</span>
                                <span>{{ log.message }}</span>
                            </div>
                            <div v-if="logs.length === 0" class="empty-logs">
                                暂无日志，加入房间后开始记录...
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

<script>
    new Vue({
        el: '#app',
        data: {
            roomId: 'room-' + Math.floor(Math.random() * 1000),
            userId: 'user' + Math.floor(1000 + Math.random() * 9000),
            localStream: null,
            remoteStream: null,
            socket: null,
            peerConnection: null,
            isConnected: false,
            remoteUserId: null,
            cameraDevices: [],
            selectedCamera: '',
            logs: [],
            callStatus: 'idle',
            connectionType: '未连接'
        },
        computed: {
            callStatusText() {
                switch (this.callStatus) {
                    case 'idle': return '等待加入房间';
                    case 'calling': return '正在建立连接';
                    case 'connected': return `已连接到 ${this.remoteUserId || '对方'}`;
                    default: return '未知状态';
                }
            }
        },
        mounted() {
            this.log('系统初始化...');
            this.enumerateDevices();
        },
        methods: {
            log(message) {
                const now = new Date();
                const time = now.toLocaleTimeString();
                this.logs.unshift({ time, message, timestamp: now.getTime() });
                if (this.logs.length > 20) this.logs.pop();
            },

            async enumerateDevices() {
                try {
                    const devices = await navigator.mediaDevices.enumerateDevices();
                    this.cameraDevices = devices.filter(d => d.kind === 'videoinput');
                    if (this.cameraDevices.length > 0) {
                        this.selectedCamera = this.cameraDevices[0].deviceId;
                    }
                    this.log(`检测到 ${this.cameraDevices.length} 个摄像头设备`);
                } catch (err) {
                    this.log('获取设备列表失败: ' + err.message);
                }
            },

            async initLocalStream() {
                try {
                    const constraints = {
                        video: {
                            deviceId: this.selectedCamera ? { exact: this.selectedCamera } : undefined,
                            width: { ideal: 1280 },
                            height: { ideal: 720 }
                        },
                        audio: true
                    };

                    this.localStream = await navigator.mediaDevices.getUserMedia(constraints);
                    this.$refs.localVideo.srcObject = this.localStream;
                    this.log('本地媒体流获取成功');
                } catch (err) {
                    this.log('获取媒体设备失败: ' + err.message);
                }
            },

            joinRoom() {
                if (!this.roomId) {
                    this.log('请输入房间ID');
                    return;
                }

                if (!this.userId) {
                    this.log('请输入用户ID');
                    return;
                }

                this.log(`尝试加入房间: ${this.roomId}`);
                this.socket = new WebSocket('ws://localhost:8080/signaling');

                // 使用箭头函数处理所有WebSocket事件
                this.socket.onopen = () => {
                    this.isConnected = true;
                    this.callStatus = 'calling';
                    this.log('WebSocket连接已建立');

                    // 发送加入房间消息
                    this.socket.send(JSON.stringify({
                        type: 'join',
                        roomId: this.roomId,
                        userId: this.userId
                    }));

                    // 初始化本地流
                    this.initLocalStream();
                };

                this.socket.onmessage = (event) => {
                    const message = JSON.parse(event.data);
                    this.log(`收到信令: ${message.type}`);

                    switch (message.type) {
                        case 'roomFull':
                            this.log('房间已满（一对一通话只能容纳两人）');
                            break;
                        case 'newUser':
                            this.handleNewUser(message.userId);
                            break;
                        case 'existingUser':
                            this.handleExistingUser(message.userId);
                            break;
                        case 'offer':
                            this.handleOffer(message.sender, message.sdp);
                            break;
                        case 'answer':
                            this.handleAnswer(message.sender, message.sdp);
                            break;
                        case 'candidate':
                            this.handleCandidate(message.sender, message.candidate);
                            break;
                        case 'userLeft':
                            this.handleUserLeft(message.userId);
                            break;
                    }
                };

                this.socket.onclose = () => {
                    this.log('WebSocket连接已关闭');
                    this.isConnected = false;
                    this.callStatus = 'idle';
                    this.handleUserLeft(this.remoteUserId);
                };

                this.socket.onerror = (error) => {
                    this.log('WebSocket错误: ' + error.message);
                };
            },

            handleNewUser(userId) {
                this.log(`新用户加入: ${userId}`);
                this.remoteUserId = userId;
                this.createPeerConnection();

                // 创建offer
                this.peerConnection.createOffer()
                    .then(offer => this.peerConnection.setLocalDescription(offer))
                    .then(() => {
                        this.socket.send(JSON.stringify({
                            type: 'offer',
                            recipient: this.remoteUserId,
                            sdp: this.peerConnection.localDescription
                        }));
                    })
                    .catch(error => {
                        this.log(`创建offer失败: ${error.message}`);
                    });
            },

            handleExistingUser(userId) {
                this.log(`已有用户在房间中: ${userId}`);
                this.remoteUserId = userId;
                this.createPeerConnection();
            },

            createPeerConnection() {
                if (this.peerConnection) {
                    return;
                }

                // 配置STUN服务器
                const config = {
                    iceServers: [
                        { urls: 'stun:stun.l.google.com:19302' },
                        { urls: 'stun:stun1.l.google.com:19302' },
                        { urls: 'stun:stun2.l.google.com:19302' }
                    ]
                };

                this.peerConnection = new RTCPeerConnection(config);

                // 添加本地流
                if (this.localStream) {
                    this.localStream.getTracks().forEach(track => {
                        this.peerConnection.addTrack(track, this.localStream);
                    });
                }

                // 使用箭头函数处理ICE候选事件
                this.peerConnection.onicecandidate = (event) => {
                    if (event.candidate && this.remoteUserId) {
                        this.socket.send(JSON.stringify({
                            type: 'candidate',
                            recipient: this.remoteUserId,
                            candidate: event.candidate
                        }));
                    }
                };

                // 使用箭头函数处理远程流事件
                this.peerConnection.ontrack = (event) => {
                    this.log(`收到来自 ${this.remoteUserId} 的远程流`);
                    this.callStatus = 'connected';
                    this.remoteStream = event.streams[0];
                    this.$refs.remoteVideo.srcObject = this.remoteStream;

                    // 检测连接类型
                    if (event.candidate) {
                        this.connectionType = event.candidate.protocol === 'udp' ? 'UDP (推荐)' : 'TCP';
                    }
                };

                // 使用箭头函数处理连接状态变化
                this.peerConnection.onconnectionstatechange = () => {
                    this.log(`连接状态: ${this.peerConnection.connectionState}`);
                };

                // 使用箭头函数处理ICE连接状态
                this.peerConnection.oniceconnectionstatechange = () => {
                    if (this.peerConnection.iceConnectionState === 'disconnected') {
                        this.log('连接已断开');
                        this.handleUserLeft(this.remoteUserId);
                    }
                };
            },

            handleOffer(senderId, offer) {
                this.log(`收到来自 ${senderId} 的offer`);
                this.remoteUserId = senderId;
                this.createPeerConnection();

                this.peerConnection.setRemoteDescription(new RTCSessionDescription(offer))
                    .then(() => this.peerConnection.createAnswer())
                    .then(answer => this.peerConnection.setLocalDescription(answer))
                    .then(() => {
                        this.socket.send(JSON.stringify({
                            type: 'answer',
                            recipient: this.remoteUserId,
                            sdp: this.peerConnection.localDescription
                        }));
                    })
                    .catch(error => {
                        this.log(`处理offer失败: ${error.message}`);
                    });
            },

            handleAnswer(senderId, answer) {
                this.log(`收到来自 ${senderId} 的answer`);
                if (this.peerConnection) {
                    this.peerConnection.setRemoteDescription(new RTCSessionDescription(answer))
                        .catch(error => {
                            this.log(`设置answer失败: ${error.message}`);
                        });
                }
            },

            handleCandidate(senderId, candidate) {
                if (this.peerConnection && candidate) {
                    this.peerConnection.addIceCandidate(new RTCIceCandidate(candidate))
                        .catch(error => {
                            this.log(`添加ICE候选失败: ${error.message}`);
                        });
                }
            },

            handleUserLeft(userId) {
                this.log(`用户离开: ${userId}`);
                if (this.peerConnection) {
                    this.peerConnection.close();
                    this.peerConnection = null;
                }

                if (this.remoteStream) {
                    this.remoteStream.getTracks().forEach(track => track.stop());
                    this.remoteStream = null;
                }

                if (this.$refs.remoteVideo) {
                    this.$refs.remoteVideo.srcObject = null;
                }

                this.remoteUserId = null;
                this.callStatus = 'calling';
                this.connectionType = '未连接';
            },

            leaveRoom() {
                if (this.socket) {
                    // 通知服务器离开房间
                    this.socket.send(JSON.stringify({
                        type: 'leave',
                        roomId: this.roomId,
                        userId: this.userId
                    }));

                    // 关闭WebSocket
                    this.socket.close();
                }

                // 关闭PeerConnection
                if (this.peerConnection) {
                    this.peerConnection.close();
                    this.peerConnection = null;
                }

                // 关闭本地流
                if (this.localStream) {
                    this.localStream.getTracks().forEach(track => track.stop());
                    this.localStream = null;
                }

                // 关闭远程流
                if (this.remoteStream) {
                    this.remoteStream.getTracks().forEach(track => track.stop());
                    this.remoteStream = null;
                }

                this.remoteUserId = null;
                this.isConnected = false;
                this.callStatus = 'idle';
                this.connectionType = '未连接';
                this.log('已离开房间');
            }
        },
        beforeDestroy() {
            this.leaveRoom();
        }
    });
</script>
</body>
</html>